package cz.data.domain.market.mapping.handler;

import com.alibaba.nacos.shaded.com.google.common.util.concurrent.RateLimiter;
import cz.data.common.core.DataConstant;
import cz.data.common.security.InMemoryRequestMatcherRepository;
import cz.data.common.utils.SpringContextHolder;
import cz.data.domain.market.mapping.listener.event.ApiEventType;
import cz.data.domain.market.mapping.listener.event.ApiReleaseEvent;
import cz.data.domain.market.model.dto.RateLimit;
import cz.data.domain.market.model.entity.DataApiEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Slf4j
@Component
public class DataApiHandlerMapping implements InitializingBean {

    private static final Map<String, DataApiEntity> mappings = new ConcurrentHashMap<>();
    private static final Map<String, RateLimiter> rateLimiters = new ConcurrentHashMap<>();

    @Resource
    RequestMappingHandlerMapping requestMappingHandlerMapping;
    @Resource
    DataApiRequestHandler handler;
    @Resource
    InMemoryRequestMatcherRepository matcherRepository;
    private Method method;

    @Override
    public void afterPropertiesSet() throws Exception {
        method = DataApiRequestHandler.class.getDeclaredMethod("invoke",
                HttpServletRequest.class, HttpServletResponse.class,
                Map.class, Map.class, Map.class);
    }

    public static DataApiEntity getMappingApiInfo(HttpServletRequest request) {
        NativeWebRequest webRequest = new ServletWebRequest(request);
        String requestMapping = (String) webRequest.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
        return getMappingApiInfo(buildMappingKey(request.getMethod(), requestMapping));
    }

    public static RateLimiter getRateLimiter(DataApiEntity api, String userId) {
        RateLimit rateLimit = api.getRateLimit();
        if (Objects.isNull(rateLimit) ||
                !DataConstant.TrueOrFalse.TRUE.getKey().equals(rateLimit.getEnable())) {
            return null;
        }
        //用户粒度限流
//        final String key = api.getId() + ":" + userId;
        //全局粒度限流
        final String key = api.getId();
        RateLimiter rateLimiter = rateLimiters.get(key);
        if (Objects.isNull(rateLimiter)) {
            synchronized (key) {
                rateLimiter = rateLimiters.get(key);
                if (Objects.isNull(rateLimiter)) {
                    rateLimiter = RateLimiter.create(rateLimit.getTimes(), Duration.ofSeconds(rateLimit.getSeconds()));
                    rateLimiters.put(key, rateLimiter);
                }
            }
        }
        return rateLimiter;
    }

    public static DataApiEntity getMappingApiInfo(String key) {
        return mappings.get(key);
    }

    public static String buildMappingKey(String requestMethod, String requestMapping) {
        return requestMethod.toUpperCase() + ":" + requestMapping;
    }

    public void registerMapping(DataApiEntity api) {
        String method = api.getReqMethod().toUpperCase();
        String path = getRequestPath(api.getApiVersion(), api.getApiUrl());
        String mappingKey = buildMappingKey(method, path);
        if (mappings.containsKey(mappingKey)) {
            // 注销接口
            this.unregisterMapping(api);
        }
        log.info("注册接口:{}", api.getApiName());
        RequestMappingInfo requestMapping = getRequestMapping(api);
        mappings.put(mappingKey, api);
        matcherRepository.register(method, path);
        requestMappingHandlerMapping.registerMapping(requestMapping, handler, this.method);
        SpringContextHolder.publishEvent(new ApiReleaseEvent(api, ApiEventType.Release));
    }

    public void unregisterMapping(DataApiEntity api) {
        String method = api.getReqMethod().toUpperCase();
        String path = getRequestPath(api.getApiVersion(), api.getApiUrl());
        String mappingKey = buildMappingKey(method, path);
        if (mappings.containsKey(mappingKey)) {
            // 注销接口
            log.info("注销接口:{}", api.getApiName());
            mappings.remove(mappingKey);
            matcherRepository.unRegister(method, path);
            String prefix = api.getId();
            List<String> keys = rateLimiters.keySet().stream().filter(prefix::startsWith).collect(Collectors.toList());
            keys.forEach(rateLimiters::remove);
            requestMappingHandlerMapping.unregisterMapping(getRequestMapping(api));
            SpringContextHolder.publishEvent(new ApiReleaseEvent(api, ApiEventType.Revoke));
        }
    }

    private RequestMappingInfo getRequestMapping(DataApiEntity api) {
        return RequestMappingInfo.paths(getRequestPath(api.getApiVersion(), api.getApiUrl()))
                .methods(RequestMethod.valueOf(api.getReqMethod().toUpperCase()))
                .produces(MediaType.APPLICATION_JSON_VALUE)
                .options(requestMappingHandlerMapping.getBuilderConfiguration())
                .build();
    }

    /**
     * 调用接口 /v1/user/1
     *
     * @param version Api版本
     * @param path    路径
     * @return Api全路径
     */
    private String getRequestPath(String version, String path) {
        String separator = "/";
        return separator + version + (path.startsWith(separator) ? path : (separator + path));
    }

}
